/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

package com.rmsl.juce;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.Pair;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.view.Choreographer;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;

import java.lang.reflect.Method;
import java.util.ArrayList;

import java.util.List;

public final class ComponentPeerView extends ViewGroup
        implements View.OnFocusChangeListener, Application.ActivityLifecycleCallbacks, Choreographer.FrameCallback
{
    public ComponentPeerView (Context context, boolean opaque_, long host)
    {
        super (context);

        if (Application.class.isInstance (context))
        {
            ((Application) context).registerActivityLifecycleCallbacks (this);
        }
        else
        {
            ((Application) context.getApplicationContext()).registerActivityLifecycleCallbacks (this);
        }

        this.host = host;
        setWillNotDraw (false);
        opaque = opaque_;

        setFocusable (true);
        setFocusableInTouchMode (true);
        setOnFocusChangeListener (this);

        // swap red and blue colours to match internal opengl texture format
        ColorMatrix colorMatrix = new ColorMatrix();

        float[] colorTransform = {0, 0, 1.0f, 0, 0,
                0, 1.0f, 0, 0, 0,
                1.0f, 0, 0, 0, 0,
                0, 0, 0, 1.0f, 0};

        colorMatrix.set (colorTransform);
        paint.setColorFilter (new ColorMatrixColorFilter (colorMatrix));

        java.lang.reflect.Method method = null;

        try
        {
            method = getClass().getMethod ("setLayerType", int.class, Paint.class);
        }
        catch (SecurityException e)
        {
        }
        catch (NoSuchMethodException e)
        {
        }

        if (method != null)
        {
            try
            {
                int layerTypeNone = 0;
                method.invoke (this, layerTypeNone, null);
            }
            catch (java.lang.IllegalArgumentException e)
            {
            }
            catch (java.lang.IllegalAccessException e)
            {
            }
            catch (java.lang.reflect.InvocationTargetException e)
            {
            }
        }

        Choreographer.getInstance().postFrameCallback (this);
    }

    public void clear()
    {
        host = 0;
    }

    //==============================================================================
    private native void handlePaint (long host, Canvas canvas, Paint paint);

    @Override
    public void onDraw (Canvas canvas)
    {
        if (host == 0)
            return;

        handlePaint (host, canvas, paint);
    }

    private native void handleDoFrame (long host, long frameTimeNanos);

    @Override
    public void doFrame (long frameTimeNanos)
    {
        if (host == 0)
            return;

        handleDoFrame (host, frameTimeNanos);

        Choreographer.getInstance().postFrameCallback (this);
    }

    @Override
    public boolean isOpaque()
    {
        return opaque;
    }

    private final boolean opaque;
    private long host;
    private final Paint paint = new Paint();

    //==============================================================================
    private native void handleMouseDown (long host, int index, float x, float y, long time);
    private native void handleMouseDrag (long host, int index, float x, float y, long time);
    private native void handleMouseUp (long host, int index, float x, float y, long time);
    private native void handleAccessibilityHover (long host, int action, float x, float y, long time);

    @Override
    public boolean onTouchEvent (MotionEvent event)
    {
        if (host == 0)
            return false;

        int action = event.getAction();
        long time = event.getEventTime();

        switch (action & MotionEvent.ACTION_MASK)
        {
            case MotionEvent.ACTION_DOWN:
                handleMouseDown (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);
                return true;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                handleMouseUp (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);
                return true;

            case MotionEvent.ACTION_MOVE:
            {
                handleMouseDrag (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);

                int n = event.getPointerCount();

                if (n > 1)
                {
                    int point[] = new int[2];
                    getLocationOnScreen (point);

                    for (int i = 1; i < n; ++i)
                        handleMouseDrag (host, event.getPointerId (i), event.getX (i) + point[0], event.getY (i) + point[1], time);
                }

                return true;
            }

            case MotionEvent.ACTION_POINTER_UP:
            {
                int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;

                if (i == 0)
                {
                    handleMouseUp (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);
                }
                else
                {
                    int point[] = new int[2];
                    getLocationOnScreen (point);

                    handleMouseUp (host, event.getPointerId (i), event.getX (i) + point[0], event.getY (i) + point[1], time);
                }
                return true;
            }

            case MotionEvent.ACTION_POINTER_DOWN:
            {
                int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;

                if (i == 0)
                {
                    handleMouseDown (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);
                }
                else
                {
                    int point[] = new int[2];
                    getLocationOnScreen (point);

                    handleMouseDown (host, event.getPointerId (i), event.getX (i) + point[0], event.getY (i) + point[1], time);
                }
                return true;
            }

            default:
                break;
        }

        return false;
    }

    @Override
    public boolean onHoverEvent (MotionEvent event)
    {
        if (accessibilityManager.isTouchExplorationEnabled())
        {
            handleAccessibilityHover (host, event.getActionMasked(), event.getRawX(), event.getRawY(), event.getEventTime());
            return true;
        }

        return false;
    }

    //==============================================================================
    public static class TextInputTarget
    {
        public TextInputTarget (long owner) { host = owner; }

        public boolean isTextInputActive()                                      { return ComponentPeerView.textInputTargetIsTextInputActive (host); }
        public int getHighlightedRegionBegin()                                  { return ComponentPeerView.textInputTargetGetHighlightedRegionBegin (host); }
        public int getHighlightedRegionEnd()                                    { return ComponentPeerView.textInputTargetGetHighlightedRegionEnd (host); }
        public void setHighlightedRegion (int b, int e)                         {        ComponentPeerView.textInputTargetSetHighlightedRegion (host, b, e); }
        public String getTextInRange (int b, int e)                             { return ComponentPeerView.textInputTargetGetTextInRange (host, b, e); }
        public void insertTextAtCaret (String text)                             {        ComponentPeerView.textInputTargetInsertTextAtCaret (host, text); }
        public int getCaretPosition()                                           { return ComponentPeerView.textInputTargetGetCaretPosition (host); }
        public int getTotalNumChars()                                           { return ComponentPeerView.textInputTargetGetTotalNumChars (host); }
        public int getCharIndexForPoint (Point point)                           { return ComponentPeerView.textInputTargetGetCharIndexForPoint (host, point); }
        public int getKeyboardType()                                            { return ComponentPeerView.textInputTargetGetKeyboardType (host); }
        public void setTemporaryUnderlining (List<Pair<Integer, Integer>> list) {        ComponentPeerView.textInputTargetSetTemporaryUnderlining (host, list); }

        //==============================================================================
        private final long host;
    }

    private native static boolean   textInputTargetIsTextInputActive (long host);
    private native static int       textInputTargetGetHighlightedRegionBegin (long host);
    private native static int       textInputTargetGetHighlightedRegionEnd (long host);
    private native static void      textInputTargetSetHighlightedRegion (long host, int begin, int end);
    private native static String    textInputTargetGetTextInRange (long host, int begin, int end);
    private native static void      textInputTargetInsertTextAtCaret (long host, String text);
    private native static int       textInputTargetGetCaretPosition (long host);
    private native static int       textInputTargetGetTotalNumChars (long host);
    private native static int       textInputTargetGetCharIndexForPoint (long host, Point point);
    private native static int       textInputTargetGetKeyboardType (long host);
    private native static void      textInputTargetSetTemporaryUnderlining (long host, List<Pair<Integer, Integer>> list);

    private native long getFocusedTextInputTargetPointer (long host);

    private TextInputTarget getFocusedTextInputTarget (long host)
    {
        final long ptr = getFocusedTextInputTargetPointer (host);
        return ptr != 0 ? new TextInputTarget (ptr) : null;
    }

    //==============================================================================
    private native void handleKeyDown (long host, int keycode, int textchar, int kbFlags);
    private native void handleKeyUp (long host, int keycode, int textchar);
    private native void handleBackButton (long host);
    private native void handleKeyboardHidden (long host);

    private static int getInputTypeForJuceVirtualKeyboardType (int type)
    {
        switch (type)
        {
            case 0:                                             // textKeyboard
                return InputType.TYPE_CLASS_TEXT
                     | InputType.TYPE_TEXT_VARIATION_NORMAL
                     | InputType.TYPE_TEXT_FLAG_MULTI_LINE
                     | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
            case 1:                                             // numericKeyboard
                return InputType.TYPE_CLASS_NUMBER
                     | InputType.TYPE_NUMBER_VARIATION_NORMAL;
            case 2:                                             // decimalKeyboard
                return InputType.TYPE_CLASS_NUMBER
                     | InputType.TYPE_NUMBER_VARIATION_NORMAL
                     | InputType.TYPE_NUMBER_FLAG_DECIMAL;
            case 3:                                             // urlKeyboard
                return InputType.TYPE_CLASS_TEXT
                     | InputType.TYPE_TEXT_VARIATION_URI
                     | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
            case 4:                                             // emailAddressKeyboard
                return InputType.TYPE_CLASS_TEXT
                     | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
                     | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
            case 5:                                             // phoneNumberKeyboard
                return InputType.TYPE_CLASS_PHONE;
            case 6:                                             // passwordKeyboard
                return InputType.TYPE_CLASS_TEXT
                     | InputType.TYPE_TEXT_VARIATION_PASSWORD
                     | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
        }

        return 0;
    }

    InputMethodManager getInputMethodManager()
    {
        return (InputMethodManager) getContext().getSystemService (Context.INPUT_METHOD_SERVICE);
    }

    public void closeInputMethodContext()
    {
        InputMethodManager imm = getInputMethodManager();

        if (imm == null)
            return;

        if (cachedConnection != null)
            cachedConnection.closeConnection();

        imm.restartInput (this);
    }

    public void showKeyboard (int virtualKeyboardType, int selectionStart, int selectionEnd)
    {
        InputMethodManager imm = getInputMethodManager();

        if (imm == null)
            return;

        // restartingInput causes a call back to onCreateInputConnection, where we'll pick
        // up the correct keyboard characteristics to use for the focused TextInputTarget.
        imm.restartInput (this);
        imm.showSoftInput (this, 0);
        keyboardDismissListener.startListening();
    }

    public void hideKeyboard()
    {
        InputMethodManager imm = getInputMethodManager();

        if (imm == null)
            return;

        imm.hideSoftInputFromWindow (getWindowToken(), 0);
        keyboardDismissListener.stopListening();
    }

    public void backButtonPressed()
    {
        if (host == 0)
            return;

        handleBackButton (host);
    }

    @Override
    public boolean onKeyDown (int keyCode, KeyEvent event)
    {
        if (host == 0)
            return false;

        // The key event may move the cursor, or in some cases it might enter characters (e.g.
        // digits). In this case, we need to reset the IME so that it's aware of the new contents
        // of the TextInputTarget.
        closeInputMethodContext();

        switch (keyCode)
        {
            case KeyEvent.KEYCODE_VOLUME_UP:
            case KeyEvent.KEYCODE_VOLUME_DOWN:
                return super.onKeyDown (keyCode, event);
            case KeyEvent.KEYCODE_BACK:
            {
                backButtonPressed();
                return true;
            }

            default:
                break;
        }

        handleKeyDown (host,
                       keyCode,
                       event.getUnicodeChar(),
                       event.getMetaState());

        return true;
    }

    @Override
    public boolean onKeyUp (int keyCode, KeyEvent event)
    {
        if (host == 0)
            return false;

        handleKeyUp (host, keyCode, event.getUnicodeChar());
        return true;
    }

    @Override
    public boolean onKeyMultiple (int keyCode, int count, KeyEvent event)
    {
        if (host == 0)
            return false;

        if (keyCode != KeyEvent.KEYCODE_UNKNOWN || (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && event.getAction() != KeyEvent.ACTION_MULTIPLE))
            return super.onKeyMultiple (keyCode, count, event);

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && event.getCharacters() != null)
        {
            int utf8Char = event.getCharacters().codePointAt (0);

            handleKeyDown (host,
                           keyCode,
                           utf8Char,
                           event.getMetaState());
            return true;
        }

        return false;
    }

    //==============================================================================
    private final class KeyboardDismissListener
    {
        public KeyboardDismissListener (ComponentPeerView viewToUse)
        {
            view = viewToUse;
        }

        private void startListening()
        {
            view.getViewTreeObserver().addOnGlobalLayoutListener (viewTreeObserver);
        }

        private void stopListening()
        {
            view.getViewTreeObserver().removeOnGlobalLayoutListener (viewTreeObserver);
        }

        private class TreeObserver implements ViewTreeObserver.OnGlobalLayoutListener
        {
            TreeObserver()
            {
                keyboardShown = false;
            }

            @Override
            public void onGlobalLayout()
            {
                Rect r = new Rect();

                View parentView = getRootView();
                int diff;

                if (parentView == null)
                {
                    getWindowVisibleDisplayFrame (r);
                    diff = getHeight() - (r.bottom - r.top);
                }
                else
                {
                    parentView.getWindowVisibleDisplayFrame (r);
                    diff = parentView.getHeight() - (r.bottom - r.top);
                }

                // Arbitrary threshold, surely keyboard would take more than 20 pix.
                if (diff < 20 && keyboardShown)
                {
                    keyboardShown = false;
                    handleKeyboardHidden (view.host);
                }

                if (! keyboardShown && diff > 20)
                    keyboardShown = true;
            }

            private boolean keyboardShown;
        }

        private final ComponentPeerView view;
        private final TreeObserver viewTreeObserver = new TreeObserver();
    }

    private final KeyboardDismissListener keyboardDismissListener = new KeyboardDismissListener (this);

    //==============================================================================
    // This implementation is quite similar to the ChangeListener in Android's built-in TextView.
    private static final class ChangeWatcher implements SpanWatcher, TextWatcher
    {
        public ChangeWatcher (ComponentPeerView viewIn, Editable editableIn, TextInputTarget targetIn)
        {
            view = viewIn;
            editable = editableIn;
            target = targetIn;

            updateEditableSelectionFromTarget (editable, target);
        }

        @Override
        public void onSpanAdded (Spannable text, Object what, int start, int end)
        {
            updateTargetRangesFromEditable (editable, target);
        }

        @Override
        public void onSpanRemoved (Spannable text, Object what, int start, int end)
        {
            updateTargetRangesFromEditable (editable, target);
        }

        @Override
        public void onSpanChanged (Spannable text, Object what, int ostart, int oend, int nstart, int nend)
        {
            updateTargetRangesFromEditable (editable, target);
        }

        @Override
        public void afterTextChanged (Editable s)
        {
        }

        @Override
        public void beforeTextChanged (CharSequence s, int start, int count, int after)
        {
            contentsBeforeChange = s.toString();
        }

        @Override
        public void onTextChanged (CharSequence s, int start, int before, int count)
        {
            if (editable != s || contentsBeforeChange == null)
                return;

            final String newText = s.subSequence (start, start + count).toString();

            int code = 0;

            if (newText.endsWith ("\n") || newText.endsWith ("\r"))
                code = KeyEvent.KEYCODE_ENTER;

            if (newText.endsWith ("\t"))
                code = KeyEvent.KEYCODE_TAB;

            target.setHighlightedRegion (contentsBeforeChange.codePointCount (0, start),
                                         contentsBeforeChange.codePointCount (0, start + before));
            target.insertTextAtCaret (code != 0 ? newText.substring (0, newText.length() - 1)
                                                : newText);

            // Treating return/tab as individual keypresses rather than part of the composition
            // sequence allows TextEditor onReturn and onTab to work as expected.
            if (code != 0)
                view.onKeyDown (code, new KeyEvent (KeyEvent.ACTION_DOWN, code));

            updateTargetRangesFromEditable (editable, target);
            contentsBeforeChange = null;
        }

        private static void updateEditableSelectionFromTarget (Editable editable, TextInputTarget text)
        {
            final int start = text.getHighlightedRegionBegin();
            final int end   = text.getHighlightedRegionEnd();

            if (start < 0 || end < 0)
                return;

            final String string = editable.toString();
            Selection.setSelection (editable,
                                    string.offsetByCodePoints (0, start),
                                    string.offsetByCodePoints (0, end));
        }

        private static void updateTargetSelectionFromEditable (Editable editable, TextInputTarget target)
        {
            final int start = Selection.getSelectionStart (editable);
            final int end   = Selection.getSelectionEnd   (editable);

            if (start < 0 || end < 0)
                return;

            final String string = editable.toString();
            target.setHighlightedRegion (string.codePointCount (0, start),
                                         string.codePointCount (0, end));
        }

        private static List<Pair<Integer, Integer>> getUnderlinedRanges (Editable editable)
        {
            final int start = BaseInputConnection.getComposingSpanStart (editable);
            final int end   = BaseInputConnection.getComposingSpanEnd   (editable);

            if (start < 0 || end < 0)
                return null;

            final String string = editable.toString();

            final ArrayList<Pair<Integer, Integer>> pairs = new ArrayList<>();
            pairs.add (new Pair<> (string.codePointCount (0, start), string.codePointCount (0, end)));
            return pairs;
        }

        private static void updateTargetCompositionRangesFromEditable (Editable editable, TextInputTarget target)
        {
            target.setTemporaryUnderlining (getUnderlinedRanges (editable));
        }

        private static void updateTargetRangesFromEditable (Editable editable, TextInputTarget target)
        {
            updateTargetSelectionFromEditable         (editable, target);
            updateTargetCompositionRangesFromEditable (editable, target);
        }

        private final ComponentPeerView view;
        private final TextInputTarget target;
        private final Editable editable;
        private String contentsBeforeChange;
    }

    private static final class Connection extends BaseInputConnection
    {
        Connection (ComponentPeerView viewIn, boolean fullEditor, TextInputTarget targetIn)
        {
            super (viewIn, fullEditor);
            view = viewIn;
            target = targetIn;
        }

        @Override
        public Editable getEditable()
        {
            if (cached != null)
                return cached;

            if (target == null)
                return cached = super.getEditable();

            int length = target.getTotalNumChars();
            String initialText = target.getTextInRange (0, length);
            cached = new SpannableStringBuilder (initialText);
            // Span the entire range of text, so that we pick up changes at any location.
            // Use cached.length rather than target.getTotalNumChars here, because this
            // range is in UTF-16 code units, rather than code points.
            changeWatcher = new ChangeWatcher (view, cached, target);
            cached.setSpan (changeWatcher, 0, cached.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            return cached;
        }

        /** Call this to stop listening for selection/composition updates.

            We do this before closing the current input method context (e.g. when the user
            taps on a text view to move the cursor), because otherwise the input system
            might send another round of notifications *during* the restartInput call, after we've
            requested that the input session should end.
        */
        @Override
        public void closeConnection()
        {
            if (cached != null && changeWatcher != null)
                cached.removeSpan (changeWatcher);

            cached = null;
            target = null;

            if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
            {
                super.closeConnection();

                if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
                    setImeConsumesInput (false);
            }
            else
            {
                finishComposingText();
            }
        }

        private ComponentPeerView view;
        private TextInputTarget target;
        private Editable cached;
        private ChangeWatcher changeWatcher;
    }

    @Override
    public InputConnection onCreateInputConnection (EditorInfo outAttrs)
    {
        TextInputTarget focused = getFocusedTextInputTarget (host);

        outAttrs.actionLabel = "";
        outAttrs.hintText = "";
        outAttrs.initialCapsMode = 0;
        outAttrs.initialSelStart = focused != null ? focused.getHighlightedRegionBegin() : -1;
        outAttrs.initialSelEnd   = focused != null ? focused.getHighlightedRegionEnd()   : -1;
        outAttrs.label = "";
        outAttrs.imeOptions = EditorInfo.IME_ACTION_UNSPECIFIED
                            | EditorInfo.IME_FLAG_NO_EXTRACT_UI
                            | EditorInfo.IME_FLAG_NO_ENTER_ACTION;
        outAttrs.inputType = focused != null ? getInputTypeForJuceVirtualKeyboardType (focused.getKeyboardType())
                                             : 0;

        cachedConnection = new Connection (this, true, focused);
        return cachedConnection;
    }

    private Connection cachedConnection;

    //==============================================================================
    @Override
    protected void onSizeChanged (int w, int h, int oldw, int oldh)
    {
        super.onSizeChanged (w, h, oldw, oldh);

        if (host != 0)
            viewSizeChanged (host);
    }

    @Override
    protected void onLayout (boolean changed, int left, int top, int right, int bottom)
    {
    }

    private native void viewSizeChanged (long host);

    @Override
    public void onFocusChange (View v, boolean hasFocus)
    {
        if (host == 0)
            return;

        if (v == this)
            focusChanged (host, hasFocus);
    }

    private native void focusChanged (long host, boolean hasFocus);

    public void setViewName (String newName)
    {
    }

    public void setSystemUiVisibilityCompat (int visibility)
    {
        Method systemUIVisibilityMethod = null;
        try
        {
            systemUIVisibilityMethod = this.getClass().getMethod ("setSystemUiVisibility", int.class);
        }
        catch (SecurityException e)
        {
            return;
        }
        catch (NoSuchMethodException e)
        {
            return;
        }
        if (systemUIVisibilityMethod == null) return;

        try
        {
            systemUIVisibilityMethod.invoke (this, visibility);
        }
        catch (java.lang.IllegalArgumentException e)
        {
        }
        catch (java.lang.IllegalAccessException e)
        {
        }
        catch (java.lang.reflect.InvocationTargetException e)
        {
        }
    }

    public boolean isVisible()
    {
        return getVisibility() == VISIBLE;
    }

    public void setVisible (boolean b)
    {
        setVisibility (b ? VISIBLE : INVISIBLE);
    }

    public boolean containsPoint (int x, int y)
    {
        return true; //xxx needs to check overlapping views
    }

    //==============================================================================
    private native void handleAppPaused (long host);
    private native void handleAppResumed (long host);

    @Override
    public void onActivityPaused (Activity activity)
    {
        if (host == 0)
            return;

        handleAppPaused (host);
    }

    @Override
    public void onActivityStopped (Activity activity)
    {

    }

    @Override
    public void onActivitySaveInstanceState (Activity activity, Bundle bundle)
    {

    }

    @Override
    public void onActivityDestroyed (Activity activity)
    {

    }

    @Override
    public void onActivityCreated (Activity activity, Bundle bundle)
    {

    }

    @Override
    public void onActivityStarted (Activity activity)
    {

    }

    @Override
    public void onActivityResumed (Activity activity)
    {
        if (host == 0)
            return;

        // Ensure that navigation/status bar visibility is correctly restored.
        handleAppResumed (host);
    }

    //==============================================================================
    private native View getNativeView (long host, int virtualViewId);
    private native boolean populateAccessibilityNodeInfo (long host, int virtualViewId, AccessibilityNodeInfo info);
    private native boolean handlePerformAction (long host, int virtualViewId, int action, Bundle arguments);
    private native Integer getInputFocusViewId (long host);
    private native Integer getAccessibilityFocusViewId (long host);

    private final class JuceAccessibilityNodeProvider extends AccessibilityNodeProvider
    {
        public JuceAccessibilityNodeProvider (ComponentPeerView viewToUse)
        {
            view = viewToUse;
        }

        @Override
        public AccessibilityNodeInfo createAccessibilityNodeInfo (int virtualViewId)
        {
            if (host == 0)
                return null;

            View nativeView = getNativeView (host, virtualViewId);

            if (nativeView != null)
                return nativeView.createAccessibilityNodeInfo();

            final AccessibilityNodeInfo nodeInfo;

            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R)
            {
                nodeInfo = new AccessibilityNodeInfo (view, virtualViewId);
            }
            else
            {
                nodeInfo = AccessibilityNodeInfo.obtain (view, virtualViewId);
            }

            if (! populateAccessibilityNodeInfo (host, virtualViewId, nodeInfo))
            {
                if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R)
                    nodeInfo.recycle();

                return null;
            }

            return nodeInfo;
        }

        @Override
        public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText (String text, int virtualViewId)
        {
            return new ArrayList<>();
        }

        @Override
        public AccessibilityNodeInfo findFocus (int focus)
        {
            if (host == 0)
                return null;

            Integer focusViewId = (focus == AccessibilityNodeInfo.FOCUS_INPUT ? getInputFocusViewId (host)
                                                                              : getAccessibilityFocusViewId (host));

            if (focusViewId != null)
                return createAccessibilityNodeInfo (focusViewId);

            return null;
        }

        @Override
        public boolean performAction (int virtualViewId, int action, Bundle arguments)
        {
            if (host == 0)
                return false;

            return handlePerformAction (host, virtualViewId, action, arguments);
        }

        private final ComponentPeerView view;
    }

    private final JuceAccessibilityNodeProvider nodeProvider = new JuceAccessibilityNodeProvider (this);
    private final AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService (Context.ACCESSIBILITY_SERVICE);

    @Override
    public AccessibilityNodeProvider getAccessibilityNodeProvider()
    {
        return nodeProvider;
    }
}
